Skip to content

Comments

Fix ObjectDisposedException when disposing session after client.StopAsync()#479

Closed
SteveSandersonMS wants to merge 1 commit intomainfrom
fix/dotnet-session-dispose-after-stop-resolved
Closed

Fix ObjectDisposedException when disposing session after client.StopAsync()#479
SteveSandersonMS wants to merge 1 commit intomainfrom
fix/dotnet-session-dispose-after-stop-resolved

Conversation

@SteveSandersonMS
Copy link
Contributor

Summary

Supersedes #371 (resolves merge conflicts against current main).
Fixes #306

When await client.StopAsync() is called before the await using var session goes out of scope, session.DisposeAsync() is called twice:

  1. First by StopAsync() (while connection is alive) ✓
  2. Second by await using cleanup (connection is now disposed) ✗ → ObjectDisposedException

Solution

Make CopilotSession.DisposeAsync() idempotent and tolerant to already-disposed connections:

  1. Added _isDisposed flag - Thread-safe using Interlocked.Exchange
  2. Made DisposeAsync idempotent - Returns immediately if already disposed (required by IAsyncDisposable contract)
  3. Handle connection failures gracefully - Catches ObjectDisposedException and IOException since dispose methods should never throw

This follows the same pattern already used in CopilotClient.DisposeAsync().

Changes

  • dotnet/src/Session.cs: Add idempotency and exception handling to DisposeAsync()
  • dotnet/test/ClientTests.cs: Add regression test

Credit to @parthtrivedi2492 for the original PR #371.

…sync()

Make CopilotSession.DisposeAsync() idempotent and tolerant to already-disposed
connections by adding an _isDisposed flag and catching ObjectDisposedException
and IOException during disposal.

Fixes #306

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@SteveSandersonMS SteveSandersonMS requested a review from a team as a code owner February 16, 2026 10:48
Copilot AI review requested due to automatic review settings February 16, 2026 10:48
@SteveSandersonMS SteveSandersonMS deleted the fix/dotnet-session-dispose-after-stop-resolved branch February 16, 2026 10:49
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes issue #306 where disposing a CopilotSession after calling client.StopAsync() results in an ObjectDisposedException. The problem occurs because StopAsync() calls session.DisposeAsync() to clean up sessions, and then when the await using var session block exits, it calls DisposeAsync() again on an already-disposed connection.

Changes:

  • Made CopilotSession.DisposeAsync() idempotent using an Interlocked.Exchange pattern
  • Added exception handling to gracefully handle already-disposed connections
  • Added regression test to verify the fix

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
dotnet/src/Session.cs Added _isDisposed flag with thread-safe check and exception handling for ObjectDisposedException and IOException in DisposeAsync()
dotnet/test/ClientTests.cs Added regression test Should_Not_Throw_When_Disposing_Session_After_Stopping_Client that reproduces the reported issue

@github-actions
Copy link

Cross-SDK Consistency Review

This PR fixes a .NET-specific issue where session.DisposeAsync() throws ObjectDisposedException when called after client.StopAsync() has already disposed the connection. The fix makes DisposeAsync() idempotent and tolerant to disposed connections.

Consistency Analysis

After reviewing all four SDK implementations, I found that the other three SDKs have the same underlying issue but haven't exposed it yet:

Current State Across SDKs:

SDK Cleanup Method Idempotent? Handles Connection Errors? Pattern
.NET (this PR) DisposeAsync() ✅ Yes ✅ Yes await using auto-calls dispose
Node.js destroy() ❌ No ❌ No Manual cleanup
Python destroy() ❌ No ❌ No Manual cleanup
Go Destroy() ❌ No ❌ No defer auto-calls

Why This Matters:

The bug surfaces when:

  1. User declares a session with automatic cleanup (await using in .NET, defer in Go)
  2. User calls client.stop() before the cleanup runs
  3. The automatic cleanup tries to destroy an already-destroyed session

Go has the same risk because of defer patterns:

session, _ := client.CreateSession(...)
defer session.Destroy() // Will be called even if Stop() is called first

// ... code ...
client.Stop() // Destroys all sessions, including this one
// When function exits, defer runs and tries to destroy again

Node.js and Python are less affected because they don't have automatic cleanup patterns as common, but could still hit issues if users manually call destroy() twice or after stop().

Recommendation

While this PR correctly fixes the .NET issue, consider adding similar defensive patterns to the other SDKs:

  1. Go - Make Destroy() idempotent and ignore connection errors (similar to .NET fix)
  2. Node.js - Make destroy() idempotent (less urgent since no auto-cleanup, but good practice)
  3. Python - Make destroy() idempotent (less urgent, but would enable async with context manager support in the future)

These changes would:

  • Prevent potential crashes in Go when using defer session.Destroy()
  • Follow the [IAsyncDisposable contract]((learn.microsoft.com/redacted) pattern across all languages
  • Make the SDKs more resilient to cleanup ordering issues

This PR is good to merge as-is for the .NET fix. The other SDKs could be addressed in follow-up PRs if desired.

AI generated by SDK Consistency Review Agent

@richlanpowers5-afk
Copy link

richlanpowers5-afk commented Feb 22, 2026 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Disposing of a CopilotSession after stopping the client results in exception

2 participants